package com.haust.detail.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.haust.commons.exception.DaoException;
import com.haust.commons.pojo.HaustResult;
import com.haust.dao.RedisDao;
import com.haust.detail.service.ItemDetailService;
import com.haust.mapper.*;
import com.haust.pojo.TbItem;
import com.haust.pojo.TbItemDesc;
import com.haust.pojo.TbItemParamItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class ItemDetailServiceImpl implements ItemDetailService {
    @Autowired
    private TbItemMapper itemMapper;

    @Autowired
    private TbItemDescMapper itemDescMapper;

    @Autowired
    private TbItemParamItemMapper itemParamItemMapper;

    @Autowired
    private RedisDao redisDao;

    @Autowired
    private ItemDescMapper detailsDescMapper;

    @Autowired
    private ItemParamItemMapper detailsParamItemMapper;


    @Value("${Haust.frontend.details.item}")
    private String itemKeyPrefix;

    @Value("${Haust.frontend.details.itemDesc}")
    private String itemDescKeyPrefix;

    @Value("${Haust.frontend.details.itemParamItem}")
    private String itemParamItemKeyPrefix;

    @Value("${Haust.frontend.detailsmultexlock.item}")
    private String mutex_key_item_prefix;

    @Value("${Haust.frontend.detailsmultexlock.itemDesc}")
    private String mutex_key_itemDesc_prefix;

    @Value("${Haust.frontend.detailsmultexlock.itemParamItem}")
    private String mutex_key_itemParamItem_prefix;


    private final Random random = new Random();
    private final Integer randSeconds = 10800;
    private final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

    /**
     * 根据主键，查询商品。 访问表格 tb_item
     * 增加缓存访问逻辑。流程是：
     * 1. 访问redis，查询缓存数据。存在则返回。
     * 2. 访问数据库，查询商品数据。查询结果保存到redis中。
     * 3. 返回。
     * <p>
     * 定义key是： 前缀 + 商品主键
     * <p>
     * 注意： 所有包含缓存访问逻辑的方法。缓存访问流程无论是否发生异常。绝对不能影响正常业务逻辑。
     * <p>
     * 分析缓存问题：
     * 1. 缓存黑洞：缓存服务器中保存的数据，必须永久保存，且这些缓存数据理论上没有上限。
     * 这种情况一旦发生，称为缓存黑洞。如：微信朋友圈。QQ说说。facebook分享等。
     * 在一些特定的环境中，不作为缓存问题。解决方便，但是有些情况不能解决。
     * <p>
     * 2. 缓存雪崩：缓存服务器中的数据，在某一时刻，大面积过期失效。导致查询逻辑，大量请求
     * 进入数据库，执行数据查询。导致数据库响应变慢，最严重造成数据库宕机。
     * 这种情况称为缓存雪崩。
     * 解决方案1，把缓存服务器中的有效时间分散。尽量保证同一时刻，只有少量数据过期失效。
     * 甚至是个别数据，过期失效。通常使用固定有效时长+-随机有效时长。
     * 解决方案2，使用定时任务，周期的查询数据库中的数据，保存到缓存服务器中，设置有效时长。
     * 有效时长，大于定时任务的周期。需要配合大数据系统实现。让大数据系统分析，什么数据需要
     * 缓存，定时任务，查询需要缓存的数据，保存到redis中。
     * <p>
     * 3. 缓存击穿： 当缓存服务器中的某数据过期失效后，有高并发的客户端请求，同时查询这一个
     * 数据。导致，高并发请求，击穿缓存，进入数据库执行查询逻辑。导致数据库响应变慢，最严重
     * 导致数据库宕机。称为缓存击穿。（有人也叫缓存穿透）
     * 解决方案，使用技术手法，让一个请求进入数据库查询数据，并把查询结果保存到缓存服务器中，
     * 其他请求，阻塞等待，阻塞结束后，从缓存服务器中查询数据并返回。
     * 使用分布式锁，让多进程、多线程环境中，只有一个请求线程，访问数据库。
     * <p>
     * 4. 缓存穿透：也可以叫读穿透。当高并发请求，查询一个缓存服务器中不存在、且数据库中也
     * 不存在的数据。无分布式锁处理。导致高并发请求，反复访问数据库，查询不存在的数据，
     * 数据库压力升高，最严重导致数据库宕机。这种情况称为缓存穿透。一般发生在恶意攻击时。
     * 解决方案，如果数据库查询结果不存在，则创建一个托底数据，短期保存在缓存服务器中。
     * 配合前端应用，做降级处理。
     *
     * @param itemId
     * @return
     */
    @Override
    public HaustResult selectItemInfo(Long itemId) {
        String key = itemKeyPrefix + itemId;

        //缓存查询失败，不要影响数据库的查询，只打印异常，不抛出异常，不返回错误
        try {
            //查找缓存数据
            TbItem cacheTbItem = redisDao.get(key);
            //如果缓存数据不为空，直接返回缓存数据
            if (cacheTbItem != null) {
                threadLocal.remove();
                return HaustResult.ok(cacheTbItem);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //设置互斥锁
        String mutex_key_item = mutex_key_item_prefix + itemId;
        try {
            if (!redisDao.setnx(mutex_key_item, "", 2L, TimeUnit.SECONDS)) {
                //判断自旋次数
                int spinCount = threadLocal.get();
                if (spinCount == 10) {
                    threadLocal.remove();
                    return HaustResult.error("服务忙，请稍后！");
                }
                //如果设置 互斥锁 不成功，自旋
                Thread.sleep(100);
                spinCount++;
                threadLocal.set(spinCount);

                return selectItemInfo(itemId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }


        //访问数据库
        TbItem tbItem = null;
        QueryWrapper<TbItem> q = new QueryWrapper<>();
        q.eq("id", itemId).eq("status", 1);
        try {
            tbItem = itemMapper.selectOne(q);
        } catch (Exception e) {
            e.printStackTrace();
            throw new DaoException("服务忙，请稍后!");
        }
        //移除threadLocal
        threadLocal.remove();

        try {
            //如果 数据库中有数据
            //缓存指定的时间，有甲方决定
            if (tbItem != null) {
                redisDao.set(key, tbItem, 7L * 24 * 60 * 60 + random.nextInt(randSeconds), TimeUnit.SECONDS);
            } else {
                //如果数据库中没数据
                TbItem nullTbItem = new TbItem();
                nullTbItem.setId(-1L);
                redisDao.set(key, nullTbItem, 2L, TimeUnit.MINUTES);
            }
            //设置缓存成功，将互斥锁释放掉
            redisDao.del(mutex_key_item);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return HaustResult.ok(tbItem);
    }


    /**
     * 通过商品id， 查找商品详情表中的信息
     *
     * @param itemId 商品id
     * @return
     */
    @Override
    public HaustResult selectItemDescByItemId(Long itemId) {
        String key = itemDescKeyPrefix + itemId;

        try {
            TbItemDesc tbItemDesc = redisDao.get(key);
            if (tbItemDesc != null) {
                threadLocal.remove();
                return HaustResult.ok(tbItemDesc);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //设置互斥锁
        String mutex_key_itemDesc = mutex_key_itemDesc_prefix + itemId;
        try {
            if (!redisDao.setnx(mutex_key_itemDesc, "", 2L, TimeUnit.SECONDS)) {
                //判断自旋次数
                int spinCount = threadLocal.get();
                if (spinCount == 10) {
                    threadLocal.remove();
                    return HaustResult.error("服务忙，请稍后！");
                }
                //如果设置 互斥锁 不成功，自旋
                Thread.sleep(100);
                spinCount++;
                threadLocal.set(spinCount);

                //如果设置 互斥锁 不成功，自旋
                return selectItemDescByItemId(itemId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //访问数据库
        TbItemDesc tbItemDesc = null;
        try {
//            tbItemDesc = itemDescMapper.selectById(itemId);
            tbItemDesc = detailsDescMapper.selectOneTbItemDesc4Status1(itemId, 1);
        } catch (Exception e) {
            e.printStackTrace();
            throw new DaoException("服务忙，请稍后！");
        }
        //移除threadLocal
        threadLocal.remove();

        try {
            //如果 数据库中有数据
            if (tbItemDesc != null) {
                redisDao.set(key, tbItemDesc, 7L * 24 * 60 * 60 + random.nextInt(randSeconds), TimeUnit.SECONDS);
            } else {
                //如果 数据库中没数据
                TbItemDesc nullTbItemDesc = new TbItemDesc();
                nullTbItemDesc.setItemId(-1L);
                redisDao.set(key, nullTbItemDesc, 2L, TimeUnit.MINUTES);
            }
            //设置缓存成功，将互斥锁释放掉
            redisDao.del(mutex_key_itemDesc);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return HaustResult.ok(tbItemDesc);
    }

    /**
     * 根据商品id，查找商品规格表中的 的具体规格信息
     *
     * @param itemId 商品id
     * @return
     */
    @Override
    public HaustResult selectTbItemParamItemByItemId(Long itemId) {
        String key = itemParamItemKeyPrefix + itemId;
        try {
            TbItemParamItem cacheTbItemParamItem = redisDao.get(key);
            if (cacheTbItemParamItem != null) {
                //移除threadLocal
                threadLocal.remove();
                return HaustResult.ok(cacheTbItemParamItem);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //设置互斥锁
        String mutex_key_itemParamItem = mutex_key_itemParamItem_prefix + itemId;
        try {
            if (!redisDao.setnx(mutex_key_itemParamItem, "", 2L, TimeUnit.SECONDS)) {
                //判断自旋次数
                int spinCount = threadLocal.get();
                if (spinCount == 10) {
                    threadLocal.remove();
                    return HaustResult.error("服务忙，请稍后！");
                }
                //如果设置 互斥锁 不成功，自旋
                Thread.sleep(100);
                spinCount++;
                threadLocal.set(spinCount);

                //如果设置 互斥锁 不成功，自旋
                return selectTbItemParamItemByItemId(itemId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //访问数据库
        QueryWrapper<TbItemParamItem> q = new QueryWrapper<>();
        q.eq("item_id", itemId);

        TbItemParamItem tbItemParamItem = null;
        try {
//            tbItemParamItem = itemParamItemMapper.selectOne(q);
            tbItemParamItem = detailsParamItemMapper.selectOneTbItemParamItem4Status1(itemId, 1);
        } catch (Exception e) {
            e.printStackTrace();
            throw new DaoException("服务忙，请稍后！");
        }
        //移除threadLocal
        threadLocal.remove();

        try {
            //如果 数据库中有数据
            if (tbItemParamItem != null) {
                redisDao.set(key, tbItemParamItem, 7L * 24 * 60 * 60 + random.nextInt(randSeconds), TimeUnit.SECONDS);
            } else {
                //如果 数据库中没数据
                TbItemParamItem nullItemParamItem = new TbItemParamItem();
                nullItemParamItem.setItemId(-1L);
                nullItemParamItem.setItemId(-1L);
                redisDao.set(key, nullItemParamItem, 2L, TimeUnit.MINUTES);
            }
            //设置缓存成功，将互斥锁释放掉
            redisDao.del(mutex_key_itemParamItem);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return HaustResult.ok(tbItemParamItem);
    }
}
